#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os
import re
import shlex
import subprocess
import time
from typing import Iterator

from saporacle.comm.common_util import logger_watched, run_subprocess
from saporacle.comm.constants import SAPOracleErrorCode
from saporacle.comm.logger import logger
from saporacle.comm.saporacle_exception import SAPOracleException
from saporacle.resource.saporacle import SAPOracle


class LogBackupSAPOracle(SAPOracle):

    """SAP on Oracle 日志备份能力"""

    def __init__(self, oracle_sid: str, sapsid: str, db_user: str = "", db_pwd: str = "") -> None:
        super().__init__(oracle_sid, sapsid, oracle_sid, sapsid, db_user, db_pwd)

    """brarchive 日志备份"""

    def query_new_log_scnlines(self, cs: bool = False) -> list[list[int]]:
        """
        :raise SAPOracleException:
        """
        tool = self._brtools_location() / "brarchive.exe"
        command = f"{tool} -c force -d util_file -k no -l E -o dist,time -q check -{'cs' if cs else 's'} -u"
        inp = f"{self._db_user}/{self._db_pwd}\n"
        file = self._brtools_outfile()
        with file.open("w", encoding="utf-8") as f:
            process = run_subprocess(command, inp=inp, stdout=f)
        return self.merged_brarchive_scnlines()

    @logger_watched(3)
    def run_brarchive(self, cs: bool = False) -> Iterator[tuple[float, float]]:
        """
        :raise SAPOracleException:
        """
        tool = self._brtools_location() / "brarchive.exe"
        command = f"{tool} -c force -d util_file -k no -l E -o dist,time -{'cs' if cs else 's'} -u"
        logger.info(command)
        try:
            with self._brtools_outfile().open("w", encoding="utf-8") as f:
                process = subprocess.Popen(shlex.split(command, posix=os.name == "posix"), text=True,
                                           stdin=subprocess.PIPE, stdout=f, stderr=subprocess.STDOUT)
        except Exception as e:
            logger.error(f"Exception occurred while creating brarchive process. {command} {e}", exc_info=1)
            raise SAPOracleException(SAPOracleErrorCode.SUBPROCESS_EXCEPTION, command, "UnknownException")
        process.stdin.write(f"{self._db_user}/{self._db_pwd}\n") and process.stdin.flush()
        while True:
            yield self._brarchive_progress()
            if process.poll() is not None:
                break
            time.sleep(30)
        cont = self._brtools_outfile().read_text(encoding="utf-8")
        # 日志备份缺少日志文件的场景，忽略报错
        if re.search(r"Offline redolog file.+not found", cont):
            logger.warn("BRARCHIVE some files not found. Ignore failure.")
            return
        if "BRARCHIVE completed successfully" not in cont:
            logger.error("BRARCHIVE not completed successfully.")
            raise SAPOracleException(SAPOracleErrorCode.SUBPROCESS_EXCEPTION, command, "UnknownOutput")
        logger.info("BRARCHIVE completed successfully.")

    def merged_brarchive_scnlines(self) -> list[list[int]]:
        """
        :raise SAPOracleException:
        """
        logfiles = [f.upper() for f in self._brarchived_logfiles()]
        _dict = {d[0].upper(): sorted([int(d[1]), int(d[2])]) for d in self._oracle_log_scnlines()}
        scnlines = sorted([_dict[f] for f in logfiles if f in _dict])
        if len(logfiles) != len(scnlines):
            logger.warn(f"No scnline of some log files. {[f for f in logfiles if f not in _dict]}")
        tmp = []
        for line in scnlines:
            if tmp and tmp[-1] >= line[0]:
                tmp[-1] = line[1]
            else:
                tmp.extend(line)
        if not tmp or len(tmp) % 2:
            logger.error(f"Merge scnlines failed. {logfiles} --> {scnlines} --> {tmp}")
            raise SAPOracleException(SAPOracleErrorCode.UNKNOWN_ERROR)
        merged = [tmp[i:i+2] for i in range(0, len(tmp), 2)]
        if 1 < len(merged):
            logger.warn(f"Merged scnlines is discontinuous. {merged}")
            return merged
        logger.info(f"Merged log scnlines: {merged}")
        return merged

    def terminate_brarchive(self) -> None:
        """
        :raise SAPOracleException:
        """
        tool = self._brtools_location() / "brarchive.exe"
        command = f"{tool} -c force -g term"
        process = run_subprocess(command)
        if "completed successfully" not in process.stdout:
            logger.error(f"Brarchive terminated Failed. {process.stdout}")
            raise SAPOracleException(SAPOracleErrorCode.SUBPROCESS_EXCEPTION, command, "UnknownOutput")
        logger.info("Brarchive terminated successfully.")

    """private"""

    def _brarchive_progress(self) -> tuple[float, float]:
        """
        :raise SAPOracleException:
        """
        file = self._brtools_outfile()
        if not file.exists():
            logger.error(f"Brarchive outfile not exists. {file.absolute()}")
            raise SAPOracleException(SAPOracleErrorCode.UNKNOWN_ERROR)
        cont = file.read_text(encoding="utf-8")
        match = re.search(r"BR0011I.+size ([\d.]+) MB", cont)
        if not match:
            logger.warn(f"Cannot match brarchive total size.")
        total_size = float(match.group(1)) if match else 0.0
        matches = re.findall(r"#SAVED.+#(\d+)", cont)
        if not matches:
            logger.warn(f"Cannot match brarchive saved size.")
        trans_size = sum((int(m) / 1024 / 1024 for m in matches), 0.0)
        return trans_size, total_size

    def _brarchived_logfiles(self) -> list[str]:
        """
        :raise SAPOracleException:
        """
        file = self._brtools_outfile()
        if not file.exists():
            logger.error(f"Brbackup outfile not exists. {file.absolute()}")
            raise SAPOracleException(SAPOracleErrorCode.UNKNOWN_ERROR)
        text = file.read_text(encoding="utf-8")
        conts = filter(lambda s: "Distribution of file" in s, re.split(r"BR\d{4}[IWE]", text))
        logfiles = sum([re.findall(r"\S+\.dbf", cont, re.IGNORECASE) for cont in conts], [])
        if not logfiles:
            logger.error("No new redolog files found.")
            raise SAPOracleException(SAPOracleErrorCode.NO_NEW_LOG)
        logger.info(f"Brarchived {len(logfiles)} logfiles: "
                    f"{logfiles if len(logfiles) <= 50 else logfiles[:25] + ['.'*24] + logfiles[-25:]}")
        return logfiles

    def _oracle_log_scnlines(self) -> list[list[str]]:
        """
        :raise SAPOracleException:
        """
        sql = "SELECT NAME,FIRST_CHANGE#,NEXT_CHANGE# FROM v$archived_log ORDER BY FIRST_CHANGE#;"
        data = self._query_data(sql)
        if not data:
            logger.error(f"Cannot query oracle log scnlines data.")
            raise SAPOracleException(SAPOracleErrorCode.UNKNOWN_ERROR)
        return data

    def log_scn_to_timestamp(self, scn: int) -> int:
        """
        :raise SAPOracleException:
        """
        sql = f"""
SELECT 
    CASE 
        WHEN FIRST_CHANGE# = {scn} THEN (CAST(FROM_TZ(CAST(FIRST_TIME AS TIMESTAMP), SESSIONTIMEZONE) AT TIME ZONE 'UTC' AS DATE) - DATE '1970-01-01') * 86400
        WHEN NEXT_CHANGE# = {scn} THEN (CAST(FROM_TZ(CAST(NEXT_TIME AS TIMESTAMP), SESSIONTIMEZONE) AT TIME ZONE 'UTC' AS DATE) - DATE '1970-01-01') * 86400
    END AS TIMESTAMP_
FROM 
    v$archived_log
WHERE 
    FIRST_CHANGE# = {scn}
    OR NEXT_CHANGE# = {scn};
"""
        data = self._query_data(sql)
        if not data:
            logger.error(f"Cannot query scn timestamp. {scn}")
            raise SAPOracleException(SAPOracleErrorCode.UNKNOWN_ERROR)
        timestamp = int(data[0][0])
        logger.info(f"scn {scn} -> timestamp {timestamp}")
        return timestamp

